﻿using System.Collections.Generic;
using System.Text;

using System.Net.DnsToolkit.Util;
using System.Net.DnsToolkit.Packet;
using System.Net.DnsToolkit.Serializer.Stream.Compression;
using System.Net.DnsToolkit.Packet.Busy;

namespace System.Net.DnsToolkit.Serializer.Stream.Extension {

    /// <summary>
    /// Extension method to stream object
    /// </summary>
    public static class StreamExtension {

        #region Read8, Read16, Read32

        /// <summary>
        /// Reads 1 byte from the stream and return byte
        /// </summary>
        /// <param name="stream"></param>
        public static byte Read8(this System.IO.Stream stream) {

            var bs = new byte[1];
            stream.Read(bs, 0, bs.Length);

            return (byte)(bs[0]);
        }

        /// <summary>
        /// Reads 2 bytes from the stream and return ushort
        /// </summary>
        /// <param name="stream"></param>
        public static ushort Read16(this System.IO.Stream stream) {

            var bs = new byte[2];
            stream.Read(bs, 0, bs.Length);

            return (ushort)(
                bs[1] << 0
                | bs[0] << 8);
        }

        /// <summary>
        /// Reads 4 bytes from the stream and return uint
        /// </summary>
        /// <param name="stream"></param>
        public static uint Read32(this System.IO.Stream stream) {

            var bs = new byte[4];
            stream.Read(bs, 0, bs.Length);

            return (uint) (
              bs[3] << 0
              | bs[2] << 8
              | bs[1] << 16
              | bs[0] << 24);
        }
        #endregion

        #region ReadDomainName

        /// <summary>
        /// 4.1.4. Message compression
        /// 
        /// In order to reduce the size of messages, the domain system utilizes a
        /// compression scheme which eliminates the repetition of domain names in a
        /// message.  In this scheme, an entire domain name or a list of labels at
        /// the end of a domain name is replaced with a pointer to a prior occurance
        /// of the same name.
        /// 
        /// The pointer takes the form of a two octet sequence:
        /// 
        ///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///     | 1  1|                OFFSET                   |
        ///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        /// 
        /// The first two bits are ones.  This allows a pointer to be distinguished
        /// from a label, since the label must begin with two zero bits because
        /// labels are restricted to 63 octets or less.  (The 10 and 01 combinations
        /// are reserved for future use.)  The OFFSET field specifies an offset from
        /// the start of the message (i.e., the first octet of the Id field in the
        /// domain header).  A zero offset specifies the first byte of the Id field,
        /// etc.
        /// 
        /// The compression scheme allows a domain name in a message to be
        /// represented as either:
        /// 
        ///    - a sequence of labels ending in a zero octet
        ///    - a pointer
        ///    - a sequence of labels ending with a pointer
        /// 
        /// Pointers can only be used for occurances of a domain name where the
        /// format is not class specific.  If this were not the case, a name server
        /// or resolver would be required to know the format of all RRs it handled.
        /// As yet, there are no such cases, but they may occur in future RDATA
        /// formats.
        /// 
        /// If a domain name is contained in a part of the message subject to a
        /// length field (such as the RDATA section of an RR), and compression is
        /// used, the length of the compressed name is used in the length
        /// calculation, rather than the length of the expanded name.
        /// 
        /// Programs are free to avoid using pointers in messages they generate,
        /// although this will reduce datagram capacity, and may cause truncation.
        /// However all programs are required to understand arriving messages that
        /// contain pointers.
        /// </summary>
        /// <param name="ds"></param>
        public static IDomainName ReadDomainName(this System.IO.Stream ds) {

            string name = ds.ReadDomainString();
            var domn = new DomainName { Name = name };
            return domn;
        }

        /// <summary>
        /// Prefer ReadDomainName as it already creates DomainName
        /// </summary>
        /// <param name="ds"></param>
        /// <returns></returns>
        public static string ReadDomainString(this System.IO.Stream ds) {

            long position = -1;
            int loop = 0;
            var pointers = new List<int>();
            var sb = new StringBuilder();
            do {

                int length = ds.Read8();
                if (length == 0) {
                    break;
                }

                bool useCompression = (length & CompressionConstant.NameCompression) == CompressionConstant.NameCompression;
                if (!useCompression) {
                    string ss = ds.ReadASCIIString(length);
                    sb.Append(ss);
                    sb.Append('.');
                    continue;
                }

                int pointer = (((length & CompressionConstant.NameMaskLength) << 8) + ds.ReadByte());
                position = (position > 0) ? position : ds.Position;

                if (pointer >= ds.Length
                    || pointers.Contains(pointer)) {
                    break;
                }

                pointers.Add(pointer);
                ds.Position = pointer;
                loop++;
                continue;

            } while (loop <= CompressionConstant.NameMaxLoop);

            if (position > 0) {
                ds.Position = position;
            }

            return sb.ToString();
        }

        #endregion

        #region ReadString

        /// <summary>
        /// Read N bytes from stream, and decodes it as ASCII
        /// </summary>
        /// <param name="ds"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        public static string ReadASCIIString(this System.IO.Stream ds, int length) {
            return ds.ReadString(length, Encoding.ASCII);
        }

        /// <summary>
        /// Read N bytes from stream, and decode using Encoding
        /// </summary>
        /// <param name="ds"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        public static string ReadString(this System.IO.Stream ds, int length, Encoding encoding) {
            var bs = new byte[length];
            ds.Read(bs, 0, length);
            string ss = encoding.GetString(bs);
            return ss;
        }

        #endregion

        #region ReadIPAddress

        /// <summary>
        /// Read n bytes from stream and computes IPAddress
        /// </summary>
        /// <param name="ds"></param>
        /// <returns></returns>
        private static IPAddress ReadIPAddress(this System.IO.Stream ds, int size) {

            var data = new byte[size];
            ds.Read(data, 0, data.Length);
            var address = new IPAddress(data);
            return address;

        }

        public static IPAddress ReadIPv4(this System.IO.Stream ds) {
            return ds.ReadIPAddress(4);
        }

        public static IPAddress ReadIPv6(this System.IO.Stream ds) {
            return ds.ReadIPAddress(16);
        }

        #endregion


        #region Write 8, 16, 32


        public static void Write8(this System.IO.Stream ds, byte value) {
            var bs = new[] { value };
            ds.Write(bs, 0, 1);

        }

        public static void Write16(this System.IO.Stream ds, ushort value) {
            var bs = new[] {
                (byte)(value >> 8),
                (byte)(value >> 0)
            };
            ds.Write(bs, 0, 2);
        }

        public static void Write32(this System.IO.Stream ds, uint value) {
            var bs = new[] {
                (byte)(value >> 24),
                (byte)(value >> 16),
                (byte)(value >>  8),
                (byte)(value >>  0)
            };
            ds.Write(bs, 0, 4);
        }

        #endregion

        #region WriteDomainName

        public static void WriteDomainName(this System.IO.Stream ds, IDomainName domainName, CompressionContext compression) {

            if (!compression.Enabled) {
                ds.WriteDomainName(domainName.Name);
                return;
            }

            CompressionData data = (CompressionData)(compression.Data);
            if (!data.SubDomains.ContainsKey(domainName)) {
                ds.WriteDomainName(domainName.Name);
                return;
            }

            // DomainName domain = compression.Find(owner, name);
            SubDomainName subDomain = data.SubDomains[domainName];
            bool straight = true;
            subDomain.ForEach(domn => {
                if (!straight) {
                    return;
                }
                if (domn.Position >= 0) {
                    straight = false;
                }
                WriteDomainName(ds, domn);
            });
            if (straight) {
                Write8(ds, 0);
            }

        }


        public static void WriteDomainName(this System.IO.Stream ds, SubDomainName domn) {

            if (domn.Position < 0) {

                domn.Position = (int)ds.Position;
                string domainName = CompressionConstant.Subdomain.Replace(domn.Name, "");

                ds.Write8((byte)domainName.Length);
                ds.WriteASCIIString(domainName);

                return;
            }

            var offset = (ushort)(domn.Position | (CompressionConstant.NameCompression << 8));
            ds.Write16(offset);

        }


        public static void WriteDomainName(this System.IO.Stream ds, string s) {

            string[] names = CompressionConstant.RegexName.Split(s);

            foreach (string name in names) {
                if (string.IsNullOrEmpty(name)) {
                    break;
                }
                ds.Write8((byte)name.Length);
                ds.WriteASCIIString(name);

            }
            ds.Write8(0);
        }

        #endregion

        #region WriteString

        public static void WriteASCIIString(this System.IO.Stream ds, string value) {
            ds.WriteString(value, Encoding.ASCII);
        }

        public static void WriteString(this System.IO.Stream ds, string value, Encoding encoding) {
            byte[] bs = encoding.GetBytes(value);
            ds.Write(bs, 0, bs.Length);
        }

        #endregion

        #region WriteIPAddress

        /// <summary>
        /// Write n bytes from stream and computes IPAddress
        /// </summary>
        /// <param name="ds"></param>
        /// <returns></returns>
        private static void WriteIPAddress(this System.IO.Stream ds, IPAddress address, int length) {

            byte[] data = address.GetAddressBytes();
            ds.Write(data, 0, length);

        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="ds"></param>
        /// <returns></returns>
        public static void WriteIPv4(this System.IO.Stream ds, IPAddress address) {
            ds.WriteIPAddress(address, 4);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="ds"></param>
        /// <returns></returns>
        public static void WriteIPv6(this System.IO.Stream ds, IPAddress address) {
            ds.WriteIPAddress(address, 16);
        }

        #endregion



    }
}
